import { Table } from '@builtIns';
import { ApiMeta } from '@components/ApiMeta.tsx';

# HtmlRspackPlugin

<ApiMeta specific={['Rspack']} />

`rspack.HtmlRspackPlugin` 是使用 Rust 实现的高性能 HTML 插件，你可以使用它来为 Rspack 项目生成 HTML 文件。

```js
new rspack.HtmlRspackPlugin(options);
```

## 对比

在使用 `rspack.HtmlRspackPlugin` 之前，请注意 `rspack.HtmlRspackPlugin` 和社区的 [`html-webpack-plugin`](https://www.npmjs.com/package/html-webpack-plugin) 插件存在一些差异。

### 性能

由于 `rspack.HtmlRspackPlugin` 是基于 Rust 实现的，因此它的构建性能显著高于 `html-webpack-plugin` 插件，尤其是在构建大量 HTML 文件的场景下。

### 功能

`rspack.HtmlRspackPlugin` 的功能是 `html-webpack-plugin` 的子集。为了保证插件的性能，我们没有实现 `html-webpack-plugin` 提供的所有功能。

如果它提供的配置项无法满足你的需求，你也可以直接使用社区的 [`html-webpack-plugin`](https://www.npmjs.com/package/html-webpack-plugin) 插件。

:::warning
`rspack.HtmlRspackPlugin` 不支持完整的 [EJS](https://github.com/mde/ejs/blob/main/docs/syntax.md) 语法, 仅支持 EJS 语法的一个子集，如果你对完整的 EJS 语法支持有需求，可以直接使用 `html-webpack-plugin`。为了和 `html-webpack-plugin` 的默认的插值语法对齐，
Rspack 修改了 EJS 的 escape 和 unescape 的默认语法，使其采用和 `html-webpack-plugin` 相同的语法。
:::

### 支持的 EJS 语法

仅支持如下基本的插值表达式、循环和判断，这里的插值表达式只支持最基本的字符串类型，不支持任意的 JavaScript 表达式，其他的 EJS 语法目前均不支持。

#### Escaped output `<%-`

对插值内容进行 escape：

```html title="ejs"
<p>Hello, <%- name %>.</p>
<p>Hello, <%- 'the Most Honorable ' + name %>.</p>
```

```json title="locals"
{
  "name": "Rspack<y>"
}
```

```html title="html"
<p>Hello, Rspack&lt;y&gt;.</p>
<p>Hello, the Most Honorable Rspack&lt;y&gt;.</p>
```

#### Unescaped output `<%=`

不对插值内容进行 escape：

```html title="ejs"
<p>Hello, <%- myHtml %>.</p>
<p>Hello, <%= myHtml %>.</p>

<p>Hello, <%- myMaliciousHtml %>.</p>
<p>Hello, <%= myMaliciousHtml %>.</p>
```

```json title="locals"
{
  "myHtml": "<strong>Rspack</strong>",
  "myMaliciousHtml": "</p><script>document.write()</script><p>"
}
```

```html title="html"
<p>Hello, &lt;strong&gt;Rspack&lt;/strong&gt;.</p>
<p>Hello, <strong>Rspack</strong>.</p>

<p>Hello, &lt;/p&gt;&lt;script&gt;document.write()&lt;/script&gt;&lt;p&gt;.</p>
<p>Hello,</p>
<script>
  document.write();
</script>
<p>.</p>
```

#### 控制语句

使用 `for in` 语句来实现列表遍历，使用 `if` 语句实现条件判断：

```txt title="ejs"
<% for tag in htmlRspackPlugin.tags.headTags { %>
  <% if tag.tagName=="script" { %>
    <%= toHtml(tag) %>
  <% } %>
<% } %>
```

## 用法

这个插件会为你生成一个 HTML 文件，该文件的 head 包含了所有 JS 产物对应的 `<script>` 标签。

只需像这样，将插件添加到你的 Rspack 配置中：

```js title="rspack.config.mjs"
import { rspack } from '@rspack/core';

export default {
  plugins: [new rspack.HtmlRspackPlugin()],
};
```

这将生成一个包含以下内容的 "_dist/index.html_" 文件：

```html
<!doctype html>
<html>
  <head>
    <meta charset="utf-8" />
    <title>rspack</title>
    <script src="main.js" defer></script>
  </head>
  <body></body>
</html>
```

如果你的 Rspack 配置中有多个 entry points，它们的生成 `<script>` 标签都会被包含在这个 HTML 文件中。

如果你的构建产物中有一些 CSS 资源，它们将被包含在 HTML head 的 `<link>` 标签中。

## 选项

你可以向 `rspack.HtmlRspackPlugin` 传递一些配置项，支持的选项如下：

- **类型：**

```ts
type HtmlRspackPluginOptions = {
  title?: string;
  filename?: string | ((entry: string) => string);
  template?: string;
  templateContent?:
    | string
    | ((params: Record<string, any>) => string | Promise<string>);
  templateParameters?:
    | Record<string, string>
    | boolean
    | ((
        params: Record<string, any>,
      ) => Record<string, any> | Promise<Record<string, any>>);
  inject?: boolean | 'head' | 'body';
  publicPath?: string;
  base?:
    | string
    | {
        href?: string;
        target?: '_self' | '_blank' | '_parent' | '_top';
      };
  scriptLoading?: 'blocking' | 'defer' | 'module' | 'systemjs-module';
  chunks?: string[];
  excludeChunks?: string[];
  chunksSortMode?: 'auto' | 'manual';
  sri?: 'sha256' | 'sha384' | 'sha512';
  minify?: boolean;
  favicon?: string;
  meta?: Record<string, string | Record<string, string>>;
  hash?: boolean;
};
```

- **默认值：** `{}`

<Table
  header={[
    {
      name: '名称',
      key: 'name',
    },
    {
      name: '类型',
      key: 'type',
    },
    {
      name: '默认值',
      key: 'default',
    },
    {
      name: '描述',
      key: 'description',
    },
  ]}
  body={[
    {
      name: '`title`',
      type: '`string | undefined`',
      default: '`undefined`',
      description: '构建 HTML 的标题',
    },
    {
      name: '`filename`',
      type: '`string | undefined | ((entry: string) => string)`',
      default: '`"index.html"`',
      description: '输出的文件名，可以指定子目录，例如 `"pages/index.html"`',
    },
    {
      name: '`template`',
      type: '`string | undefined`',
      default: '`undefined`',
      description: '模版文件路径，支持 ejs',
    },
    {
      name: '`templateContent`',
      type: '`string | undefined | ((params: Record<string, any>) => string | Promise<string>)`',
      default: '`undefined`',
      description:
        '模版文件内容，优先级大于 template，使用函数时传入渲染参数并将返回的字符串作为模板内容',
    },
    {
      name: '`templateParameters`',
      type: '`Record<string, string> | undefined | boolean | ((params: Record<string, any>) => Record<string, any> | Promise<Record<string, any>>)`',
      default: '`undefined`',
      description:
        '传递给模版的参数，使用函数时传入渲染参数，并将返回的内容作为最终的渲染参数',
    },
    {
      name: '`inject`',
      type: '`boolean | undefined | "head" | "body"`',
      default: '`true`',
      description:
        '产物注入位置，使用 `false` 则不注入，不指定时则会根据 `scriptLoading` 的配置自动判断',
    },
    {
      name: '`publicPath`',
      type: '`string | undefined`',
      default: '`undefined`',
      description: 'script 和 link 标签的 publicPath',
    },
    {
      name: '`base`',
      type: '`string | undefined | { href?: string; target?: "_self" | "_blank" | "_parent" | "_top" }`',
      default: '`undefined`',
      description:
        '注入一个 [`base`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/base) 标签',
    },
    {
      name: '`scriptLoading`',
      type: '`"blocking" | "defer" | "module" | "systemjs-module" | undefined`',
      default: '`"defer"`',
      description:
        '现代浏览器支持使用 `defer` 来异步加载 JavaScript，设置为 `"module"` 则会添加 `type="module"` 同时使用 `defer`',
    },
    {
      name: '`chunks`',
      type: '`string[] | undefined`',
      default: '`undefined`',
      description: '配置需要注入的 chunk，默认注入所有 chunk',
    },
    {
      name: '`excludeChunks`',
      type: '`string[] | undefined`',
      default: '`undefined`',
      description: '配置需要跳过注入的 chunk',
    },
    {
      name: '`chunksSortMode`',
      type: '`"auto" | "manual"`',
      default: '`"auto"`',
      description: '配置 chunk 的排序模式',
    },
    {
      name: '`sri`',
      type: '`"sha256"" | "sha384"" | "sha512"" | undefined`',
      default: '`undefined`',
      description:
        '<p>**已废弃**：请使用 [`SubresourceIntegrityPlugin`](./subresource-integrity-plugin) 代替。</p><p>配置 SRI hash 算法，默认不开启 SRI。</p>',
    },
    {
      name: '`minify`',
      type: '`boolean`',
      default: '`undefined`',
      description: '是否启用压缩',
    },
    {
      name: '`favicon`',
      type: '`string | undefined`',
      default: '`undefined`',
      description: '配置 HTML 图标',
    },
    {
      name: '`meta`',
      type: '`Record<string, string | Record<string, string>>`',
      default: '`{}`',
      description: '配置需要注入 HTML 的 meta',
    },
    {
      name: '`hash`',
      type: '`boolean`',
      default: '`undefined`',
      description:
        '是否在生成加载路径时添加 compilation 的哈希值作为后缀，以让缓存失效',
    },
  ]}
/>

## 示例

### 自定义 HTML 模板

如果默认生成的 HTML 不符合你的需求，你可以使用自己的模板。

#### 使用模板文件

最简单的方式是使用 `template` 选项，并传递一个自定义的 HTML 文件。`rspack.HtmlRspackPlugin` 将会自动将所有需要的 JS、CSS 和 favicon 文件注入到 HTML 中。

通过 `template` 指定 HTML 模板文件：

```html title="index.html"
<!doctype html>
<html>
  <head>
    <meta charset="utf-8" />
    <title><%= htmlRspackPlugin.options.title %></title>
  </head>
  <body></body>
</html>
```

```js title="rspack.config.mjs"
import { rspack } from '@rspack/core';

export default {
  plugins: [
    new rspack.HtmlRspackPlugin({
      title: "My HTML Template"
      template: 'index.html',
    }),
  ],
};
```

#### 使用模板字符串

通过 `templateContent` 指定 HTML 模板内容：

```js title="rspack.config.mjs"
import { rspack } from '@rspack/core';

export default {
  plugins: [
    new rspack.HtmlRspackPlugin({
      title: "My HTML Template"
      templateContent: `
        <!DOCTYPE html>
        <html>
          <head>
            <title><%= htmlRspackPlugin.options.title %></title>
          </head>
          <body></body>
        </html>
      `,
    }),
  ],
};
```

#### 使用模板生成函数

可通过传入一个获取 HTML 模板内容的函数来实现自定义的模板生成逻辑：

- 在 `templateContent` 中传入函数

```js title="rspack.config.mjs"
import { rspack } from '@rspack/core';

export default {
  plugins: [
    new rspack.HtmlRspackPlugin({
      title: "My HTML Template"
      templateContent: ({ htmlRspackPlugin }) => `
        <!DOCTYPE html>
        <html>
          <head>
            <title>${htmlRspackPlugin.options.title}</title>
          </head>
          <body></body>
        </html>
      `,
    }),
  ],
};
```

- 或在 `template` 中传入一个 `.js` 或 `.cjs` 结尾的文件路径

```js title="template.js"
module.exports = ({ htmlRspackPlugin }) => `
  <!DOCTYPE html>
  <html>
    <head>
      <title>${htmlRspackPlugin.options.title}</title>
    </head>
    <body></body>
  </html>
`;
```

```js title="rspack.config.mjs"
import { rspack } from '@rspack/core';

export default {
  plugins: [
    new rspack.HtmlRspackPlugin({
      title: "My HTML Template"
      template: "template.js",
    }),
  ],
};
```

#### 模板渲染参数

可通过 `templateParameters` 扩展 HTML 模板渲染参数。以下变量在模板中默认可用：

- `htmlRspackPlugin`: 插件的数据
  - `htmlRspackPlugin.options`: 插件的配置对象
  - `htmlRspackPlugin.tags`: 准备好的用于在模板中注入的标签信息
    - `htmlRspackPlugin.tags.headTags`: 用于在 `<head>` 中注入的 `<base>`、`<meta>`、`<link>`、`<script>` 标签列表
    - `htmlRspackPlugin.tags.bodyTags`: 用于在 `<body>` 中注入的 `<script>` 标签列表
  - `htmlRspackPlugin.files`: 此次编译产生的产物文件信息
    - `htmlRspackPlugin.files.js`: 此次编译产生的 JS 产物路径列表
    - `htmlRspackPlugin.files.css`: 此次编译产生的 CSS 产物路径列表
    - `htmlRspackPlugin.files.favicon`: 若配置了 `favicon`，此处为计算出的最终的 favicon 产物路径
    - `htmlRspackPlugin.files.publicPath`: 产物文件的 publicPath
- `rspackConfig`: 此次编译所使用的 Rspack 配置对象
- `compilation`: 此次编译的 compilation 对象

:::warning 警告
若使用 `htmlRspackPlugin.tags` 在模板渲染时插入标签，请将 `inject` 配置为 `false`，否则会导致标签被注入两次。
:::

:::info 差异
以下内容与 HtmlWebpackPlugin 存在差异：

- 不支持使用 `!` 来添加 loader 处理模板文件
- `rspackConfig` 对象目前仅支持获取 `mode`、`output.publicPath` 和 `output.crossOriginLoading` 属性
- `compilation` 对象目前仅支持在[使用模板生成函数](#使用模板生成函数)时使用
- 在模板中渲染标签列表（如 `htmlRspackPlugin.tags.headTags`）或单个标签（如 `htmlRspackPlugin.tags.headTags[0]`）时，需要使用 `toHtml()` 函数生成 HTML 代码

:::

### 过滤 Chunks

可以通过如下配置指定需要注入的 chunk：

```js title="rspack.config.mjs"
import { rspack } from '@rspack/core';

export default {
  plugins: [
    new HtmlRspackPlugin({
      chunks: ['app'],
    }),
  ],
};
```

也可以通过如下配置排除掉特定的 chunk：

```js title="rspack.config.mjs"
import { rspack } from '@rspack/core';

export default {
  plugins: [
    new HtmlRspackPlugin({
      excludeChunks: ['app'],
    }),
  ],
};
```

### Meta 标签

如果设置了 `meta`，HtmlRspackPlugin 将注入 `<meta>` 标签。

> 请查看这份维护良好的几乎所有可用的 [meta 标签](https://github.com/joshbuchea/HEAD#meta)的列表。

通过如下配置添加键值对以生成 `<meta>` 标签:

```js title="rspack.config.mjs"
import { rspack } from '@rspack/core';

export default {
  plugins: [
    new HtmlRspackPlugin({
      meta: {
        // 将会生成: <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
        viewport: 'width=device-width, initial-scale=1, shrink-to-fit=no',
        // 将会生成: <meta name="theme-color" content="#4285f4">
        'theme-color': '#4285f4',
        // 将会生成:  <meta http-equiv="Content-Security-Policy" content="default-src https:">
        'Content-Security-Policy': {
          'http-equiv': 'Content-Security-Policy',
          content: 'default-src https:',
        },
      },
    }),
  ],
};
```

### Base 标签

如果设置了 `base`，HtmlRspackPlugin 将注入 `<base>` 标签。

> 关于 `<base>` 标签的更多信息，请查看 [文档](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/base)

可以通过如下配置生成 `<base>` 标签：

```js
new HtmlWebpackPlugin({
  // 将会生成: <base href="http://example.com/some/page.html">
  base: 'http://example.com/some/page.html',
});

new HtmlWebpackPlugin({
  // 将会生成: <base href="http://example.com/some/page.html" target="_blank">
  base: {
    href: 'http://example.com/some/page.html',
    target: '_blank',
  },
});
```

### 生成多个 HTML 文件

如果你有多个 entry points，并希望为每个 entry 生成一个 HTML 文件，那么你可以注册多个 `rspack.HtmlRspackPlugin`：

- 使用 `filename` 来为每个 HTML 文件指定名称。
- 使用 `chunks` 来为每个 HTML 文件指定需要包含的 JS 产物。

比如以下配置，会生成 foo.html 和 bar.html，其中 foo.html 仅会包含 foo.js 生成的 JS 产物。

```js title="rspack.config.mjs"
import { rspack } from '@rspack/core';

export default {
  entry: {
    foo: './foo.js',
    bar: './bar.js',
  },
  plugins: [
    new rspack.HtmlRspackPlugin({
      filename: 'foo.html',
      chunks: ['foo'],
    }),
    new rspack.HtmlRspackPlugin({
      filename: 'bar.html',
      chunks: ['bar'],
    }),
  ],
};
```

## Hooks

HtmlRspackPlugin 提供了一些 hooks，可以让你在构建过程中修改标签或 HTML 产物代码。可通过 `rspack.HtmlRspackPlugin.getCompilationHooks` 来获取 hooks 对象：

```js title="rspack.config.mjs"
const HtmlModifyPlugin = {
  apply(compiler) {
    compiler.hooks.compilation.tap('HtmlModifyPlugin', compilation => {
      const hooks = HtmlRspackPlugin.getCompilationHooks(compilation);
      // hooks.beforeAssetTagGeneration.tapPromise()
      // hooks.alterAssetTags.tapPromise()
      // hooks.alterAssetTagGroups.tapPromise()
      // hooks.afterTemplateExecution.tapPromise()
      // hooks.beforeEmit.tapPromise()
      // hooks.afterEmit.tapPromise()
    });
  },
};

export default {
  // ...
  plugins: [new HtmlRspackPlugin(), HtmlModifyPlugin],
};
```

### beforeAssetTagGeneration

从 compilation 中收集产物信息，生成加载路径后，生成标签标签前调用。

可在此处修改 `assets` 来添加增加自定义的 JS、CSS 产物。

- **类型：** `AsyncSeriesWaterfallHook<[BeforeAssetTagGenerationData]>`
- **参数：**
  ```ts
  type BeforeAssetTagGenerationData = {
    assets: {
      publicPath: string;
      js: Array<string>;
      css: Array<string>;
      favicon?: string;
    };
    outputName: string;
    plugin: {
      options: HtmlRspackPluginOptions;
    };
  };
  ```

:::warning 警告
仅 `assets.js`、`assets.css` 和 `assets.favicon` 可修改，其他项目的修改将不会生效。
:::

如下代码将添加了一个额外的 `extra-script.js` 并在最终产物中生成对应的 `<script defer src="extra-script.js"></script>` 标签

```js title="rspack.config.mjs"
const AddScriptPlugin = {
  apply(compiler) {
    compiler.hooks.compilation.tap('AddScriptPlugin', compilation => {
      const hooks = HtmlRspackPlugin.getCompilationHooks(compilation);
      hooks.beforeAssetTagGeneration.tapPromise(
        'AddScriptPlugin',
        async data => {
          data.assets.js.push('extra-script.js');
        },
      );
    });
  },
};

export default {
  // ...
  plugins: [new HtmlRspackPlugin(), AddScriptPlugin],
};
```

### alterAssetTags

基于产物路径信息生成产物标签后，确定标签插入位置前调用。可在此处调整标签的信息。

- **类型：** `AsyncSeriesWaterfallHook<[AlterAssetTagsData]>`
- **参数：**

  ```ts
  type HtmlTag = {
    tagName: string;
    attributes: Record<string, string | boolean | undefined | null>;
    voidTag: boolean;
    innerHTML?: string;
    asset?: string;
  };

  type AlterAssetTagsData = {
    assetTags: {
      scripts: Array<HtmlTag>;
      styles: Array<HtmlTag>;
      meta: Array<HtmlTag>;
    };
    outputName: string;
    plugin: {
      options: HtmlRspackPluginOptions;
    };
  };
  ```

:::warning 警告
仅 `assetTags` 可修改，其他项目的修改将不会生效。
:::

- 若修改属性值为 `true` 时将添加无值属性，将生成 `<script defer specialattribute src="main.js"></script>`
- 若修改属性值为 `string` 时将添加有值属性，将生成 `<script defer specialattribute="some value" src="main.js"></script>`
- 若修改属性值为 `false` 时移除该属性

如下代码将给所有 `script` 类型的标签添加 `specialAttribute` 属性:

```js title="rspack.config.mjs"
const AddAttributePlugin = {
  apply(compiler) {
    compiler.hooks.compilation.tap('AddAttributePlugin', compilation => {
      const hooks = HtmlRspackPlugin.getCompilationHooks(compilation);
      hooks.alterAssetTags.tapPromise('AddAttributePlugin', async data => {
        data.assetTags.scripts = data.assetTags.scripts.map(tag => {
          if (tag.tagName === 'script') {
            tag.attributes.specialAttribute = true;
          }
          return tag;
        });
      });
    });
  },
};

export default {
  // ...
  plugins: [new HtmlRspackPlugin(), AddAttributePlugin],
};
```

### alterAssetTagGroups

在生成标签分组到 `head` 和 `body` 后，模板被函数或模板引擎渲染前调用。可在此处调整标签的插入位置。

- **类型：** `AsyncSeriesWaterfallHook<[AlterAssetTagGroupsData]>`
- **参数：**
  ```ts
  type AlterAssetTagGroupsData = {
    headTags: Array<HtmlTag>;
    bodyTags: Array<HtmlTag>;
    outputName: string;
    plugin: {
      options: HtmlRspackPluginOptions;
    };
  };
  ```

:::warning 警告
仅 `headTags` 和 `bodyTags` 可修改，其他项目的修改将不会生效。
:::

如下代码将把 `async` 的 `script` 标签从 `body` 调整到 `head` 中:

```js title="rspack.config.mjs"
const MoveTagsPlugin = {
  apply(compiler) {
    compiler.hooks.compilation.tap('MoveTagsPlugin', compilation => {
      const hooks = HtmlRspackPlugin.getCompilationHooks(compilation);
      hooks.alterAssetTagGroups.tapPromise('MoveTagsPlugin', async data => {
        data.headTags.push(data.headTags.bodyTags.filter(i => i.async));
        data.bodyTags = data.bodyTags.filter(i => !i.async);
      });
    });
  },
};

export default {
  // ...
  plugins: [
    new HtmlRspackPlugin({
      inject: 'body',
    }),
    AllHeadTagsPlugin,
  ],
};
```

### afterTemplateExecution

在模板渲染完成后，标签注入前调用。可在此处修改 HTML 内容和将被注入的标签。

- 当使用函数 `templateContent` 或 `.js/.cjs` 结尾的 `template`，使用该函数渲染模板，此处 `html` 为函数返回的结果
- 其他场景会将 HTML 模板通过模板引擎编译，此处 `html` 为编译后的结果

- **类型：** `AsyncSeriesWaterfallHook<[AfterTemplateExecutionData]>`
- **参数：**
  ```ts
  type AfterTemplateExecutionData = {
    html: string;
    headTags: Array<HtmlTag>;
    bodyTags: Array<HtmlTag>;
    outputName: string;
    plugin: {
      options: HtmlRspackPluginOptions;
    };
  };
  ```
  :::warning 警告
  仅 `html`、`headTags` 和 `bodyTags` 可修改，其他项目的修改将不会生效。
  :::

如下代码将在 body 结尾添加 `Injected by plugin`，之后标签才会注入并添加到该文本之后，因此产物中为 ，产物中为 `Injected by plugin<script defer src="main.js"></script></body>`：

```js title="rspack.config.mjs"
const InjectContentPlugin = {
  apply(compiler) {
    compiler.hooks.compilation.tap('InjectContentPlugin', compilation => {
      const hooks = HtmlRspackPlugin.getCompilationHooks(compilation);
      hooks.afterTemplateExecution.tapPromise(
        'InjectContentPlugin',
        async pluginArgs => {
          pluginArgs.html = pluginArgs.html.replace(
            '</body>',
            'Injected by plugin</body>',
          );
        },
      );
    });
  },
};

export default {
  // ...
  plugins: [
    new HtmlRspackPlugin({
      inject: 'body',
    }),
    InjectContentPlugin,
  ],
};
```

### beforeEmit

在生成 HTML 产物前调用，修改 HTML 产物内容的最终机会。

- **类型：** `SyncHook<[BeforeEmitData]>`
- **参数：**
  ```ts
  type BeforeEmitData = {
    html: string;
    outputName: string;
    plugin: {
      options: HtmlRspackPluginOptions;
    };
  };
  ```

:::warning 警告
仅 `html` 可修改，其他项目的修改将不会生效。
:::

如下代码将在 body 结尾添加 `Injected by plugin`，产物中为 `<script defer src="main.js"></script>Injected by plugin</body>`：

```js title="rspack.config.mjs"
const InjectContentPlugin = {
  apply(compiler) {
    compiler.hooks.compilation.tap('InjectContentPlugin', compilation => {
      const hooks = HtmlRspackPlugin.getCompilationHooks(compilation);
      hooks.beforeEmit.tapPromise('InjectContentPlugin', async data => {
        data.html = data.html.replace('</body>', 'Injected by plugin</body>');
      });
    });
  },
};

export default {
  // ...
  plugins: [
    new HtmlRspackPlugin({
      inject: 'body',
    }),
    InjectContentPlugin,
  ],
};
```

### afterEmit

在生成 HTML 产物后调用，仅用于事件通知。

- **类型：** `SyncHook<[AfterEmitData]>`
- **参数：**
  ```ts
  type AfterEmitData = {
    outputName: string;
    plugin: {
      options: HtmlRspackPluginOptions;
    };
  };
  ```
